package com.stars.easyms.base.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.ConcurrentReferenceHashMap;

import java.io.Closeable;
import java.io.Externalizable;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

/**
 * <p>className: AnnotationUtil</p>
 * <p>description: 注解工具类</p>
 *
 * @author guoguifang
 * @version 1.2.1
 * @date 2019/7/4 12:25
 */
@SuppressWarnings("unchecked")
@Slf4j
public final class AnnotationUtil {

    private static final Map<Class<?>, Set<Method>> ANNOTATED_BASE_TYPE_CACHE = new ConcurrentReferenceHashMap<>(256);

    private static final Map<AnnotatedElement, Annotation[]> DECLARED_ANNOTATIONS_CACHE = new ConcurrentReferenceHashMap<>(256);

    private static final Set<Class<?>> JAVA_LANGUAGE_INTERFACES = new HashSet<>(Arrays.asList(Serializable.class, Externalizable.class,
            Closeable.class, AutoCloseable.class, Cloneable.class, Comparable.class));

    public static <A extends Annotation> void setAnnotationFieldMap(Method method, Class<A> annotationType, Map<String, Object> map) {
        Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
        setAnnotationFieldMap(annotation, map);
    }

    public static <A extends Annotation> void setAnnotationFieldValue(Method method, Class<A> annotationType, String fieldName, Object value) {
        Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
        setAnnotationFieldValue(annotation, fieldName, value);
    }

    public static void setAnnotationFieldMap(Annotation annotation, Map<String, Object> map) {
        try {
            InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);
            Field field = invocationHandler.getClass().getDeclaredField("memberValues");
            field.setAccessible(true);
            Map memberValues = (Map) field.get(invocationHandler);
            memberValues.putAll(map);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            log.error("Set annotation [{}] field[{}] failure!", annotation.annotationType(), map.keySet());
        }
    }

    public static void setAnnotationFieldValue(Annotation annotation, String fieldName, Object value) {
        try {
            InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);
            Field field = invocationHandler.getClass().getDeclaredField("memberValues");
            field.setAccessible(true);
            Map memberValues = (Map) field.get(invocationHandler);
            memberValues.put(fieldName, value);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            log.error("Set annotation [{}] field[{}] failure!", annotation.annotationType(), fieldName);
        }
    }

    public static boolean hasAnnotation(AnnotatedElement element, Class<? extends Annotation> annotationType) {
        if (element.isAnnotationPresent(annotationType)) {
            return true;
        }
        return Boolean.TRUE.equals(searchWithFindSemantics(element, annotationType));
    }

    public static boolean isInJavaLangAnnotationPackage(@Nullable String annotationType) {
        return (annotationType != null && annotationType.startsWith("java.lang.annotation"));
    }

    @Nullable
    private static <T> T searchWithFindSemantics(AnnotatedElement element, @Nullable Class<? extends Annotation> annotationType) {
        return searchWithFindSemantics(element, (annotationType != null ? Collections.singleton(annotationType) : Collections.emptySet()));
    }

    @Nullable
    private static <T> T searchWithFindSemantics(AnnotatedElement element, Set<Class<? extends Annotation>> annotationTypes) {
        try {
            return searchWithFindSemantics(element, annotationTypes, new HashSet<>(), 0);
        } catch (Exception ex) {
            throw new IllegalStateException("Failed to introspect annotations on " + element, ex);
        }
    }

    @Nullable
    private static <T> T searchWithFindSemantics(AnnotatedElement element, Set<Class<? extends Annotation>> annotationTypes, Set<AnnotatedElement> visited, int metaDepth) {
        if (visited.add(element)) {
            try {
                Annotation[] annotations = getDeclaredAnnotations(element);
                if (annotations.length > 0) {
                    for (Annotation annotation : annotations) {
                        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                        if (!isInJavaLangAnnotationPackage(currentAnnotationType) && annotationTypes.contains(currentAnnotationType)) {
                            return (T) Boolean.TRUE;
                        }
                    }

                    for (Annotation annotation : annotations) {
                        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                        if (!hasPlainJavaAnnotationsOnly(currentAnnotationType)) {
                            T result = searchWithFindSemantics(currentAnnotationType, annotationTypes, visited, metaDepth + 1);
                            if (result != null) {
                                return result;
                            }
                        }
                    }
                }

                if (element instanceof Method) {
                    Method method = (Method) element;
                    T result;

                    Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
                    if (resolvedMethod != method) {
                        result = searchWithFindSemantics(resolvedMethod, annotationTypes, visited, metaDepth);
                        if (result != null) {
                            return result;
                        }
                    }

                    Class<?>[] ifcs = method.getDeclaringClass().getInterfaces();
                    if (ifcs.length > 0) {
                        result = searchOnInterfaces(method, annotationTypes, visited, metaDepth, ifcs);
                        if (result != null) {
                            return result;
                        }
                    }

                    Class<?> clazz = method.getDeclaringClass();
                    while (true) {
                        clazz = clazz.getSuperclass();
                        if (clazz == null || clazz == Object.class) {
                            break;
                        }
                        Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(clazz);
                        if (!annotatedMethods.isEmpty()) {
                            for (Method annotatedMethod : annotatedMethods) {
                                if (isOverride(method, annotatedMethod)) {
                                    Method resolvedSuperMethod = BridgeMethodResolver.findBridgedMethod(annotatedMethod);
                                    result = searchWithFindSemantics(resolvedSuperMethod, annotationTypes, visited, metaDepth);
                                    if (result != null) {
                                        return result;
                                    }
                                }
                            }
                        }
                        result = searchOnInterfaces(method, annotationTypes, visited, metaDepth, clazz.getInterfaces());
                        if (result != null) {
                            return result;
                        }
                    }
                } else if (element instanceof Class) {
                    Class<?> clazz = (Class<?>) element;
                    if (!Annotation.class.isAssignableFrom(clazz)) {
                        for (Class<?> ifc : clazz.getInterfaces()) {
                            T result = searchWithFindSemantics(ifc, annotationTypes, visited, metaDepth);
                            if (result != null) {
                                return result;
                            }
                        }
                        Class<?> superclass = clazz.getSuperclass();
                        if (superclass != null && superclass != Object.class) {
                            T result = searchWithFindSemantics(superclass, annotationTypes, visited, metaDepth);
                            if (result != null) {
                                return result;
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                handleIntrospectionFailure(element, ex);
            }
        }
        return null;
    }

    private static boolean isInJavaLangAnnotationPackage(@Nullable Class<? extends Annotation> annotationType) {
        return (annotationType != null && isInJavaLangAnnotationPackage(annotationType.getName()));
    }

    @Nullable
    private static <T> T searchOnInterfaces(Method method, Set<Class<? extends Annotation>> annotationTypes, Set<AnnotatedElement> visited, int metaDepth, Class<?>[] ifcs) {

        for (Class<?> ifc : ifcs) {
            Set<Method> annotatedMethods = getAnnotatedMethodsInBaseType(ifc);
            if (!annotatedMethods.isEmpty()) {
                for (Method annotatedMethod : annotatedMethods) {
                    if (isOverride(method, annotatedMethod)) {
                        T result = searchWithFindSemantics(annotatedMethod, annotationTypes, visited, metaDepth);
                        if (result != null) {
                            return result;
                        }
                    }
                }
            }
        }

        return null;
    }

    private static Set<Method> getAnnotatedMethodsInBaseType(Class<?> baseType) {
        boolean ifcCheck = baseType.isInterface();
        if (ifcCheck && JAVA_LANGUAGE_INTERFACES.contains(baseType)) {
            return Collections.emptySet();
        }

        Set<Method> annotatedMethods = ANNOTATED_BASE_TYPE_CACHE.get(baseType);
        if (annotatedMethods != null) {
            return annotatedMethods;
        }
        Method[] methods = (ifcCheck ? baseType.getMethods() : baseType.getDeclaredMethods());
        for (Method baseMethod : methods) {
            try {
                if ((ifcCheck || !Modifier.isPrivate(baseMethod.getModifiers())) && hasSearchableAnnotations(baseMethod)) {
                    if (annotatedMethods == null) {
                        annotatedMethods = new HashSet<>();
                    }
                    annotatedMethods.add(baseMethod);
                }
            } catch (Exception ex) {
                handleIntrospectionFailure(baseMethod, ex);
            }
        }
        if (annotatedMethods == null) {
            annotatedMethods = Collections.emptySet();
        }
        ANNOTATED_BASE_TYPE_CACHE.put(baseType, annotatedMethods);
        return annotatedMethods;
    }

    private static boolean hasPlainJavaAnnotationsOnly(@Nullable Object annotatedElement) {
        Class<?> clazz;
        if (annotatedElement instanceof Class) {
            clazz = (Class<?>) annotatedElement;
        } else if (annotatedElement instanceof Member) {
            clazz = ((Member) annotatedElement).getDeclaringClass();
        } else {
            return false;
        }
        String name = clazz.getName();
        return (name.startsWith("java") || name.startsWith("org.springframework.lang."));
    }

    private static boolean isOverride(Method method, Method candidate) {
        if (!candidate.getName().equals(method.getName()) ||
                candidate.getParameterCount() != method.getParameterCount()) {
            return false;
        }
        Class<?>[] paramTypes = method.getParameterTypes();
        if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
            return true;
        }
        for (int i = 0; i < paramTypes.length; i++) {
            if (paramTypes[i] != ResolvableType.forMethodParameter(candidate, i, method.getDeclaringClass()).resolve()) {
                return false;
            }
        }
        return true;
    }

    private static boolean hasSearchableAnnotations(Method ifcMethod) {
        Annotation[] anns = getDeclaredAnnotations(ifcMethod);
        if (anns.length == 0) {
            return false;
        }
        for (Annotation ann : anns) {
            String name = ann.annotationType().getName();
            if (!name.startsWith("java.lang.") && !name.startsWith("org.springframework.lang.")) {
                return true;
            }
        }
        return false;
    }

    private static Annotation[] getDeclaredAnnotations(AnnotatedElement element) {
        if (element instanceof Class || element instanceof Member) {
            return DECLARED_ANNOTATIONS_CACHE.computeIfAbsent(element, AnnotatedElement::getDeclaredAnnotations);
        }
        return element.getDeclaredAnnotations();
    }

    private static void handleIntrospectionFailure(@Nullable AnnotatedElement element, Throwable ex) {
        if (element instanceof Class && Annotation.class.isAssignableFrom((Class<?>) element)) {
            if (log.isDebugEnabled()) {
                log.debug("Failed to meta-introspect annotation {}.", element, ex);
            }
        } else {
            if (log.isInfoEnabled()) {
                log.info("Failed to introspect annotations on {}.", element, ex);
            }
        }
    }

    private AnnotationUtil() {
    }
}
